還記得我們最一開始是怎麼處理 routing 嗎?
程式碼大概是像這樣
class App
def call(env)
if env['REQUEST_METHOD'] == 'GET' && env['PATH_INFO'] == '/hello'
[200, { 'Content-Type' => 'text/plain' }, ['hello']]
else
[404, { 'Content-Type' => 'text/plain' }, ['Not found']]
end
end
end
run App.new
利用簡單的 if...else
來判斷該回傳什麼樣的 Response,但我們都知道對於現實生活中的需求,routing 可沒那麼簡單,如果要針對每一個路徑都寫一次 if...else
的判斷,想想那是多麼可怕的一件事情
在實作 routing 之前,不知道大家有沒有用過另一個 Ruby 比較輕型的網頁框架 - Sinatra,他的 routing 寫法像是這樣
# 官方範例
# myapp.rb
require 'sinatra'
get '/' do
'Hello world!'
end
用 DSL 的方式來撰寫 routing,我們今天就先來仿照實作一個 mini 版的 Sinatra routing
一樣建立一個新的資料夾,裡面放 config.ru,程式碼如下
# demo/config.ru
class MiniSinatra
class Route
attr_accessor :method, :path, :block
def initialize(method, path, block)
@method = method
@path = path
@block = block
end
def match?(env)
env['REQUEST_METHOD'] == method && env['PATH_INFO'] == path
end
end
def initialize
@routes = []
end
def add_route(method, path, &block)
@routes << Route.new(method, path, block)
end
def call(env)
route = @routes.detect { |route| route.match?(env) }
if route
body = route.block.call
[200, { 'Content-Type' => 'text/plain' }, [body]]
else
[404, { 'Content-Type' => 'text/plain' }, ['Not found']]
end
end
end
app = MiniSinatra.new
app.add_route('GET', '/') { 'hello world !' }
app.add_route('GET', '/lobster') { 'hello lobster !' }
run app
我們建立了一個叫 MiniSinatra
的 Rack Application,接著在裡面又建立了一個 Route
的 class
class Route
attr_accessor :method, :path, :block
def initialize(method, path, block)
@method = method
@path = path
@block = block
end
def match?(env)
env['REQUEST_METHOD'] == method && env['PATH_INFO'] == path
end
end
在 Route
裡面我們接收了三個參數,method
和 path
會用來比對這個 request
(也就是 env
裡面所包含的資訊) 與這個 route
是不是符合
def match?(env)
env['REQUEST_METHOD'] == method && env['PATH_INFO'] == path
end
另外我們在 MiniSinatra
這個 application 加了一個 add_route,將 route 規則存放在 @routes 裡面,加規則的寫法就像是這樣
app.add_route('GET', '/') { 'hello world !' }
app.add_route('GET', '/lobster') { 'hello lobster !' }
接著打開瀏覽器就可以測試結果,瀏覽對應的 URL,應該會有不同的文字印在畫面上
例如瀏覽 http://127.0.0.1:3001/
會出現 hello world!
,瀏覽 http://127.0.0.1:3001/lobster
,就會出現 hello lobster !
這樣的寫法雖然也可行,但並沒有發揮到 Ruby 方便實作 DSL 的優勢,我們需要再修改一下
為了要做到用 DSL 來寫 Routing,我們需要用一個變數來紀錄 Application,並且在 MiniSinatra
加上一個 class method
# demo/config.ru
def self.application
@application ||= MiniSinatra.new
end
接著在寫一個 get
method 在 MiniSinatra
外面,像是這樣
# demo/config.ru
class MiniSinatra
class Route
# .
# .
# (略)
end
end
def get(path, &block)
MiniSinatra.application.add_route('GET', path, &block)
end
透過建立一個 get
的 helper method,並且傳遞 &block
方式將 block 傳到方法裡面做處理,我們就做出了一個簡單的 DSL
然後在底下我們就可以用 Sinatra 的寫法來加上 Routing 規則
# demo/config.ru
get '/' do
'hello world !'
end
get '/lobster' do
'hello lobster !'
end
run MiniSinatra.application
是不是感覺蠻容易的?
最後整段程式碼就會長這樣
class MiniSinatra
class Route
attr_accessor :method, :path, :block
def initialize(method, path, block)
@method = method
@path = path
@block = block
end
def match?(env)
env['REQUEST_METHOD'] == method && env['PATH_INFO'] == path
end
end
def initialize
@routes = []
end
def add_route(method, path, &block)
@routes << Route.new(method, path, block)
end
def call(env)
route = @routes.detect { |route| route.match?(env) }
if route
body = route.block.call
[200, { 'Content-Type' => 'text/plain' }, [body]]
else
[404, { 'Content-Type' => 'text/plain' }, ['Not found']]
end
end
def self.application
@application ||= MiniSinatra.new
end
end
def get(path, &block)
MiniSinatra.application.add_route('GET', path, &block)
end
get '/' do
'hello world !'
end
get '/lobster' do
'hello lobster !'
end
run MiniSinatra.application
利用這個觀念,明天我們就來改善 Mavericks 的 Routing 的寫法